﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using Foundation.Sugar;
using Protocol.Data;

namespace Protocol.Transport
{
    public abstract class DataChannel : TcpSocketChannel
    {
        protected int PackageNumber;
        public byte[] AuthorizationKey { get; set; }
        public ulong ServerTime { get; set; }
        public byte[] Salt { get; set; }
        protected long SessionId = BitConverter.ToInt64(Guid.NewGuid().ToByteArray(), 0);
        protected int SeqNo;
        protected byte[] KeyHash;

        public void Send(object data)
        {
            var dataBytes = data.ToBytes();
            var messageBytes = EncaseData(dataBytes);
            var packageBytes = EncaseMessage(messageBytes);
            var result = base.Send(packageBytes);
            if (result == SocketError.Success) PackageNumber++;
            else throw new Exception(result.ToString());
            msgsAck.msg_ids.Clear();
        }

        public object Receive()
        {
            byte[] packageBytes;
            var result = Receive(out packageBytes);
            if (result != SocketError.Success) 
                throw new Exception(result.ToString());
            var messageBytes = UncaseMessage(packageBytes);
            var dataBytes = UncaseData(messageBytes);
            var data = dataBytes.FromBytes();
            if (data.Is<msg_container>())
            {
                var session = data.As<msg_container>().messages.OfType<new_session_created>().First();
                Salt = BitConverter.GetBytes(session.server_salt);
                SessionId = session.unique_id;
                //SeqNo = session.first_msg_id;
            }

            msgsAck.Send(this);

            return data;
        }

        protected IList<byte> EncaseData(IList<byte> bytes)
        {
            var messageBytes = new List<byte>();
            if (AuthorizationKey == null)
            {
                messageBytes.AddRange(BitConverter.GetBytes(0UL));
                var time = TimeManager.GetTime();
                var timeStamp = time.ToUnixTimeStamp();
                timeStamp += (timeStamp % 4 + 4);
                //var time = (ulong) (DateTime.Now.ToUnixTimeStamp()/4)*4;
                messageBytes.AddRange(BitConverter.GetBytes(timeStamp));
                messageBytes.AddRange(BitConverter.GetBytes(bytes.Count));
                messageBytes.AddRange(bytes);
            }
            else
            {
                messageBytes.AddRange(Salt);
                messageBytes.AddRange(BitConverter.GetBytes(SessionId));
                var time = TimeManager.GetTime();
                var timeStamp = (long)time.ToUnixTimeStamp();
                var timeStampd = time.ToUnixTimeStampBytes();
                //timeStamp += (timeStamp % 4 + 4);
               // var time = ServerTime;//(ulong)(DateTime.Now.ToUnixTimeStamp() / 4) * 4;
                var time1 = (long)ServerTime;
                time1 = (time1 << 32) + SeqNo*4;
               
                messageBytes.AddRange(BitConverter.GetBytes(time1));
                messageBytes.AddRange(BitConverter.GetBytes(SeqNo++*2+1));
                messageBytes.AddRange(BitConverter.GetBytes(bytes.Count));
                messageBytes.AddRange(bytes);

                var messageKey = Substring(Hash(messageBytes), 4, 16);
                var encriptedData = Encrypt(messageBytes.AlignBy(16), messageKey, AuthorizationKey);
                var result = new List<byte>();
                if (KeyHash == null) KeyHash = Hash(AuthorizationKey).Skip(12).Take(8).ToArray();
                result.AddRange(KeyHash);
                result.AddRange(messageKey);
                result.AddRange(encriptedData);
                messageBytes = result;
            }

            return messageBytes;
        }

        protected IList<byte> UncaseData(IList<byte> bytes)
        {
            if (bytes.Count < 24)
                return bytes;

            if (AuthorizationKey == null)
                return bytes.Skip(20).ToArray();

            var message = bytes.Skip(24).ToArray();
            if (AuthorizationKey == null)
                return message;

            var keyHash = bytes.Skip(0).Take(8).ToArray();
            if (!keyHash.SequenceEqual(KeyHash))
                throw new Exception("Invalid key hash");

            var messageKey = bytes.Skip(8).Take(16).ToArray();
            var data = Decrypt(message, messageKey, AuthorizationKey);

            var salt = data.Take(8).ToArray();
            var sessionId = data.Skip(8).Take(8).ToArray();
            var messageId = data.Skip(16).Take(8).ToArray();

            if (!Salt.SequenceEqual(salt))
                throw new Exception("Invalid salt");
            var sid = BitConverter.ToInt64(sessionId, 0);
          //  if (SessionId != sid)
            //    throw new Exception("Invalid session id");

            //var seqNo = data.Skip(24).Take(4).ToArray();
            var messageDataLen = BitConverter.ToInt32(data.Skip(28).Take(4).ToArray(), 0);
            var messageData = data.Skip(32).Take(messageDataLen).ToList();

            var hash = data.Substring(0, 32 + messageDataLen).Hash().Substring(4, 16);
            if (!hash.SequenceEqual(messageKey))
                throw new Exception("Invalid message hash");

            msgsAck.msg_ids.Add(BitConverter.ToInt32(messageId, 0));

            return messageData;
        }

        private new readonly msgs_ack msgsAck = new msgs_ack {msg_ids = new Vector<long>()};

        protected static byte[] Crypt(byte[] messageData, byte[] messageKey, byte[] authorizationKey, bool directMode, bool fromServer)
        {
            var x = fromServer ? 8 : 0;
            var a = Hash(messageKey, Substring(authorizationKey, x, 32));
            var b = Hash(Substring(authorizationKey, 32 + x, 16), messageKey, Substring(authorizationKey, 48 + x, 16));
            var c = Hash(Substring(authorizationKey, 64 + x, 32), messageKey);
            var d = Hash(messageKey, Substring(authorizationKey, 96 + x, 32));
            var aesKey = Concat(Substring(a, 0, 8), Substring(b, 8, 12), Substring(c, 4, 12));
            var aesIv = Concat(Substring(a, 8, 12), Substring(b, 0, 8), Substring(c, 16, 4), Substring(d, 0, 8));
            var cryptedData = directMode
                ? messageData.AES256IGEEncrypt(aesIv, aesKey)
                : messageData.AES256IGEDecrypt(aesIv, aesKey);
            return cryptedData;
        }

        protected static byte[] Encrypt(byte[] messageData, byte[] messageKey, byte[] authorizationKey, int x = 0)
        {
            return Crypt(messageData, messageKey, authorizationKey, true, false);
        }

        protected static byte[] Decrypt(byte[] messageData, byte[] messageKey, byte[] authorizationKey, int x = 8)
        {
            return Crypt(messageData, messageKey, authorizationKey, false, true);
        }

        protected abstract byte[] EncaseMessage(IList<byte> bytes);

        protected abstract byte[] UncaseMessage(IList<byte> bytes);
    }
}